<!--
  - 拉曼-训练后台
 -->

<!-- 视图层 -->
<template><header-comp />

<!-- TFjs初始化组件 -->
<tfjsInit />

<!-- 业务介绍：卡片容器 -->
<uni-card title="业务介绍">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "操作方法：
    balabala"
  />
  <!-- 文字 -->
  <view><text>
    　　专门训练模型用的后台管理界面。\n
    　　其它介绍待完善。
  </text></view>
</uni-card>

<!-- 模型 -->
<uni-card
  v-if="tfBackendRef"
  title="模型"
>
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "新建模型，或加载预训练模型。会展示模型的拓扑结构概览。"
  />

  <!-- 二级标题：加载新模型 -->
  <uni-section
    v-if="!isGetModel"
    title="加载新模型"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字 -->
    <view><text>
      　　模型介绍待完善。
    </text></view>
  </uni-section>

  <!-- 按钮及容器 -->
  <view class="center">
    <!-- 加载模型按钮 -->
    <button
      :disabled="isGetModel"
      @click="buildNewModel()"
      size="mini" type="primary"
    >
      {{ isGetModel ? "加载完成" : "加载模型" }}
    </button>
    <!-- 查看模型概述按钮 -->
    <button
      v-if="isGetModel"
      @click="showVisor()"
      size="mini" type="primary"
    >
      查看模型概述
    </button>
  </view>

  <!-- 二级标题：读取已有模型 -->
  <uni-section
    v-if="!isGetModel"
    title="读取已有模型"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字 -->
    <view><text>
      　　需要拓扑结构文件和权重参数文件。\n
      　　注意：本应用生成的拓扑结构文件，其匹配的权重参数文件名应为“model.weights.bin”，千万不要修改文件名，以免校验报错。
    </text></view>
    <!-- 上传模型文件容器 -->
    <view class="center">
      <!-- 拓扑结构文件 -->
      <uni-file-picker
        @select="(event) => readModelFile(event.tempFiles[0])"
        class="center"
        file-extname="json" fileMediatype="all" limit="1" return-type="object" 
        :del-icon="false" :auto-upload="false" :disable-preview="true"
      >
        <!-- 文件上传按钮 -->
        <button size="mini" type="primary">
          读取拓扑文件
        </button>
      </uni-file-picker>
      <!-- 权重参数文件 -->
      <uni-file-picker
        @select="(event) => readModelFile(event.tempFiles[0])"
        class="center"
        file-extname="bin" fileMediatype="all" limit="1" return-type="object"
        :del-icon="false" :auto-upload="false" :disable-preview="true"
      >
        <!-- 文件上传按钮 -->
        <button size="mini" type="primary">
          读取权重文件
        </button>
      </uni-file-picker>
    </view>
  </uni-section>

</uni-card>

<!-- 数据集 -->
<uni-card
  v-if="tfBackendRef"
  title="数据集"
>
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "读取xlsx格式文件，特征应为1600条，标签应为2条。"
  />
  <!-- 文字 -->
  <view><text>
    　　读取xlsx格式文件，特征应为1600条，标签应为2条。\n
    　　标签应预先归一化。
  </text></view>
  <!-- 文件上传容器 -->
  <uni-file-picker
    @select="(event) => readDatasetFile(event.tempFiles[0].file)"
    :readonly="isGetDataset"
    class="center"
    file-extname="xlsx, xls" fileMediatype="all" limit="1" return-type="object"
    :del-icon="false" :auto-upload="false" :disable-preview="true"
  >
    <!-- 文件上传按钮 -->
    <button size="mini" type="primary">
      {{ isGetDataset ? "已获得数据集" : "读取xlsx数据集文件" }}
    </button>
  </uni-file-picker>
</uni-card>

<!-- 训练 -->
<uni-card title="训练">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "可设定批次样本数、迭代次数、训练集乱序、验证集等。训练完成后可获取拟合记录。"
  />
  <!-- 二级标题：参数说明 -->
  <uni-section
    title="参数说明"
    type="circle"
    padding="0 1em"
  >
    <!-- 折叠面板及下拉框 -->
    <uni-collapse><uni-collapse-item
      title="点击下拉查看参数说明"
    >
      <!-- 二级标题：批次样本数 -->
      <uni-section
        title="批次样本数"
        type="line"
        padding="0 1em"
      >
        <!-- 文字 -->
        <view><text>
          　　批次样本数，即batchSize。\n
          　　海量的数据集不可能一次性全被读取进GPU缓存区，所以数据其实是“一批一批地”被带入模型训练的。每一批数据的大小，即批次样本数。\n
          　　批次样本数一般为2的整数幂（如32、16、8、4等），以对齐运算资源，优化计算效率。一般而言，批次样本数越大，训练完一轮全部数据的速度就越快，当然也就越消耗内存。\n
          　　建议打开任务管理器，关注训练时的内存开销，选择内存占用适中的批次样本数。
        </text></view>
      </uni-section>
      <!-- 二级标题：迭代次数 -->
      <uni-section
        title="迭代次数"
        type="line"
        padding="0 1em"
      >
        <!-- 文字 -->
        <view><text>
          　　迭代次数，即epochs。\n
          　　神经网络遵循以学习率为超参的迭代学习规则，仅仅遍历一轮全部训练集数据很难达到令人满意的训练效果。合适的模型往往需要使用全部训练集数据进行重复上千次的训练拟合。\n
          　　正式训练时的迭代次数一般为某个整数（如100、数百、1000等）。迭代次数越大，训练时间就越长，也越容易达到令人满意的训练效果，但也可能造成过拟合。\n
          　　正式训练前，也往往先用较小的迭代次数（如10、20、50等）尝试训练，观察可能的计算性能、拟合变化等情况，以更好的估算迭代次数。批次样本数越大，训练完一轮全部数据的速度就越快，当然也就越消耗内存。
        </text></view>
      </uni-section>
      <!-- 二级标题：训练集乱序 -->
      <uni-section
        title="训练集乱序"
        type="line"
        padding="0 1em"
      >
        <!-- 文字 -->
        <view><text>
          　　训练集乱序可防止训练集自身因数据排序而存在的“序列规律”导致的过拟合。
        </text></view>
      </uni-section>
      <!-- 二级标题：验证集提取间隔 -->
      <uni-section
        title="验证集提取间隔"
        type="line"
        padding="0 1em"
      >
        <!-- 文字 -->
        <view><text>
          　　一般而言，会从数据集中划分出一部分数据作为验证集，用于在训练过程中评估模型的泛化能力。验证集的数据不会在训练集中出现。\n
          　　因本项目数据序列具有一定规律，故提取验证集的算法为：规定[验证集提取间隔]，每隔一定间隔从数据集中提取一个样本，作为验证集。间隔越大，验证集越少，反之越多。\n
          　　当[验证集提取间隔]为0时，不提取验证集。
        </text></view>
      </uni-section>
    </uni-collapse-item></uni-collapse>
  </uni-section>
  <!-- 二级标题：调参训练 -->
  <uni-section
    title="调参训练"
    type="circle"
    padding="0 1em"
  >
    <!-- 输入框的表格容器 -->
    <uni-table
      :border="true" :stripe="true"
    >
      <!-- 标题行 -->
      <uni-tr>
        <uni-th align="center">
          键
        </uni-th>
        <uni-th align="center">
          值
        </uni-th>
      </uni-tr>
      <!-- 内容行：批次样本数 -->
      <uni-tr>
        <uni-td align="center">
          批次样本数
        </uni-td>
        <!-- 批次样本数输入框及容器 -->
        <uni-td><uni-easyinput
          v-model="batches"
          type="number"
          placeholder="请输入批次样本数"
          trim="all" :clearable="false"
        /></uni-td>
      </uni-tr>
      <!-- 内容行：迭代次数 -->
      <uni-tr>
        <uni-td align="center">
          迭代次数
        </uni-td>
        <!-- 迭代次数输入框及表格容器 -->
        <uni-td><uni-easyinput
          v-model="epochCounts"
          type="number"
          placeholder="请输入迭代次数"
          trim="all" :clearable="false"
        /></uni-td>
      </uni-tr>
      <!-- 内容行：训练集乱序 -->
      <uni-tr>
        <uni-td align="center">
          训练集乱序
        </uni-td>
        <!-- 是否乱序开关及表格容器 -->
        <uni-td align="center">
          否　<switch
            :disabled="isOnTrain"
            :checked="isShuffle"
            @change="(event) => { isShuffle = event.detail.value }"
            type="switch"
          />　是
        </uni-td>
      </uni-tr>
      <!-- 内容行：验证集提取间隔 -->
      <uni-tr>
        <uni-td align="center">
          验证集提取间隔
        </uni-td>
        <!-- 验证集提取间隔输入框及表格容器 -->
        <uni-td><uni-easyinput
          v-model="valPerCount"
          type="number"
          placeholder="请输入验证集提取间隔"
          trim="all" :clearable="false"
        /></uni-td>
      </uni-tr>
    </uni-table>
    <!-- 训练按钮及容器 -->
    <view class="center"><button
      :disabled="!isGetDataset || !isGetModel || isOnTrain"
      @click="trainModel()"
      size="mini" type="primary"
    >
      训练模型
    </button></view>
  </uni-section>
  <!-- 二级标题：训练记录 -->
  <uni-section
    v-if="logArrRef[0]"
    title="计算历程"
    type="circle"
    padding="0 1em"
  >
    <!-- 计算历程：表格容器 -->
    <uni-table border stripe>
      <!-- 表格标题行 -->
      <uni-tr>
        <uni-th align="center">序号</uni-th>
        <uni-th>内容</uni-th>
      </uni-tr>
      <!-- 表格内容行 -->
      <uni-tr v-for="(log, index) in logArrRef" :key="index">
        <uni-td align="center">{{ index }}</uni-td>
        <uni-td><text>{{ log }}</text></uni-td>
      </uni-tr>
    </uni-table>
    <!-- 打开图表板容器及按钮 -->
    <view class="center"><button
      @click="showVisor()"
      size="mini" type="primary"
    >
      打开图表板
    </button></view>
  </uni-section>
  <!-- 二级标题：模型下载 -->
  <uni-section
    v-if="isGetModel"
    title="模型下载"
    type="circle"
    padding="0 1em"
  >
    <!-- 下载模型容器及按钮 -->
    <view class="center"><button
      @click="downloadModel()"
      size="mini" type="primary"
    >
      下载模型[拓扑结构]&[权重参数]
    </button></view>
    <!-- 下载训练记录容器及按钮 -->
    <view class="center"><button
      v-if="isTrained"
      @click="saveTrainLog()"
      size="mini" type="primary"
    >
      下载训练历程数据
    </button></view>
  </uni-section>
</uni-card>

</template>

<!-- 逻辑层 -->
<script setup>
// 导入vue的框架方法
import { shallowRef, ref } from "vue"
// 导入tfjs和tfjs-visor库
import * as tf from "@tensorflow/tfjs"
import * as tfvis from "@tensorflow/tfjs-vis"
// 导入xlsx库，以XLSX作为引用对象
import * as XLSX from "xlsx"
// 导入TF初始化组件
import tfjsInit from "../index/tfjs-init.vue"
// 导入公共方法库的方法
import {
  // tfjs计算后端挂载完毕的Ref对象
  tfBackendRef,
  // 显示图表板的方法
  showVisor,
  // 读取xlsx文件为workbook对象
  readXlsxFile,
  // 下载训练历程到xlsx文件
  downloadTrainHistoryToXlsx,
  // // 可视化模型概要
  // showModelSummary,
  // // 可视化模型各层
  // showModelLayers
} from "../../scripts/app_common.js"
// 导入模型方法
import { createCnnModel } from "../../scripts/models/raman-cnn.js"


/**
 * @ref对象
 */
// 训练记录
const logArrRef = ref([])
// 是否获取模型
const isGetModel = shallowRef(false)
// 是否获取数据集
const isGetDataset = shallowRef(false)
// 是否正在训练
const isOnTrain = shallowRef(false)
// 是否训练
const isTrained = shallowRef(false)
// 批次样本数
const batches = shallowRef(32)
// 训练迭代次数
const epochCounts = shallowRef(10)
// 每多少个数据提取一个验证集
const valPerCount = shallowRef(0.1)
// 是否乱序
const isShuffle = shallowRef(true)

/**
 * @其他对象
 */
// 模型
const modelObj = {
  // 模型
  model: null,
  // 模型拓扑结构文件
  modelJsonFile: null,
  // 模型权重参数文件
  modelWeightsFile: null,
}
// 数据集
const TensorObj = {
  // 训练集
  feature: null,
  // 标签集
  label: null,
  // 用于恢复的4个归一化数据
  featureMax: null,
  featureMin: null,
  labelMax: null,
  labelMin: null,
}
// 训练
const trainObj = {
  // 记录
  log: null
}

/**
 * @buildNewModel 建立新的模型
 * @function
 */
async function buildNewModel() { try {
  // 直接通过导入方法建立模型
  const model = createCnnModel()
  // 打印模型结构
  model.summary()
  // showModelSummary(model, "新建模型概述")
  // 输出模型及参数
  modelObj.model = model
  // 已获取模型的ref标签
  isGetModel.value = true
} catch (error) {
  console.error("buildNewModel()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "建立模型报错",
    content: error,
  })
}}

/**
 * @readModelFile 读取模型(文件)
 * 包含读取JSON文件和权重文件
 * @function
 * @param {{
 *   file: File
 *   extname: String
 * }} fileMapObj 封装的文件对象
 */
async function readModelFile(fileMapObj) { try {
  // 显示加载圈
  uni.showLoading({ title: "正在读取文件..." })
  // 根据文件后缀名，分配拓扑文件和权重文件
  if (fileMapObj.extname === "json") {
    modelObj.modelJsonFile = fileMapObj.file
  } else if (fileMapObj.extname === "bin") {
    modelObj.modelWeightsFile = fileMapObj.file
  }
  // 如果两个文件都传了，就开始解析
  if (modelObj.modelJsonFile && modelObj.modelWeightsFile) {
    const model = await tf.loadLayersModel(
      tf.io.browserFiles([modelObj.modelJsonFile, modelObj.modelWeightsFile])
    )
    // 可视化模型概要
    model.summary()
    // showModelSummary(model, "已有模型概述")
    // showModelLayers(model, "模型参数")
    // 输出模型参数
    modelObj.model = model
    // 删掉文件对象节省内存
    modelObj.modelJsonFile = null
    modelObj.modelWeightsFile = null
    // 已获取模型标签
    isGetModel.value = true
  }
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("readModelFile()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "读取模型(文件)报错",
    content: error,
  })
}}

/**
 * @readDatasetFile 读取数据集文件
 * excel表格，包含数据文件名和标签
 * @function
 * @param {{ File }} file 封装的文件对象
 */
async function readDatasetFile(file) { try {
  // 显示加载圈
  uni.showLoading({ title: "正在读取文件数据" })
  // 读取文件为工作表对象
  const workbook = await readXlsxFile(file)
  // 将工作表转为AOA数据
  const sheetDataAoa = XLSX.utils.sheet_to_json(
    workbook.Sheets[workbook.SheetNames[0]],
    {
      // 标题设置为“1模式”，即AOA呈现
      header: 1,
      // 不允许空白行，遇空白行则跳过
      blankrows: false
    }
  )
  // 提取第一行标签的最后一个值，用于判断是标题还是值
  const valueForTest = sheetDataAoa[0][sheetDataAoa[0].length - 1]
  // 如果第一行标签的最后一个值不是值，则移除第一行
  if (valueForTest && isNaN(Number(valueForTest))) {
    sheetDataAoa.shift(1)
  }
  // 解构赋值，处理数据得到张量。用tf.tidy()减少内存占用
  const [
    featureTensor,
    labelTensor,
    featureMax,
    featureMin,
    labelMax,
    labelMin
  ] = tf.tidy(() => {
    // 构造数据集张量
    const dataTensorRaw = tf.tensor2d(
      // 原数据，即sheetDataAoa
      sheetDataAoa,
      // 张量尺寸
      [sheetDataAoa.length, sheetDataAoa[0].length]
    )
    // 解构赋值，拆分数据集张量
    const [featureTensorRaw, labelTensorRaw] = tf.split(
      // 被拆分的张量
      dataTensorRaw,
      // 拆分方法，shape[0]为样品数，shape[1]为总特征+总标签数
      [dataTensorRaw.shape[1] - 2, 2],
      // 沿着[1]特征轴分割
      1
    )
    // 归一化。这里全局归一化即可
    // 先找全局的最大值和最小值
    const featureMax = tf.max(featureTensorRaw)
    const featureMin = tf.min(featureTensorRaw)
    const labelMax = tf.max(labelTensorRaw)
    const labelMin = tf.min(labelTensorRaw)
    // 用最大值和最小值做归一化
    const featureTensor = featureTensorRaw.sub(featureMin)
      .div(featureMax.sub(featureMin))
    const labelTensor = labelTensorRaw.sub(labelMin)
      .div(labelMax.sub(labelMin))
    // 返回特征张量、标签张量及归一化数据
    return [
      featureTensor,
      labelTensor,
      featureMax,
      featureMin,
      labelMax,
      labelMin
    ]
  })
  // 解构赋值之后，把数据输出传给全局对象
  TensorObj.feature = featureTensor
  TensorObj.label = labelTensor
  TensorObj.featureMax = featureMax
  TensorObj.featureMin = featureMin
  TensorObj.labelMax = labelMax
  TensorObj.labelMin = labelMin
  // 修改ref对象的值
  isGetDataset.value = true
  // // 打印
  // TensorObj.feature.print(true)
  // TensorObj.label.print(true)
  // TensorObj.featureMax.print(true)
  // TensorObj.featureMin.print(true)
  // TensorObj.labelMax.print(true)
  // TensorObj.labelMin.print(true)
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("readDatasetFile()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "读取数据集文件报错",
    content: error,
  })
}}


/**
 * @trainModel 模型的训练
 * 先编译模型，再训练模型
 * @function
 * @note 为了确保性能，不用try-catch包裹
 */
async function trainModel() {
  // ref标记
  isOnTrain.value = true
  // 编译模型
  modelObj.model.compile({
    // 优化器：Adam
    optimizer: tf.train.adam(),
    // 损失函数：均方误差
    loss: tf.losses.meanSquaredError,
    // 评估指标：平均绝对误差
    metrics: [tf.metrics.meanAbsoluteError],
  })
  // 提取训练参数
  // 每次迭代训练的样本数
  const batchesValue = Math.floor(Number(batches.value))
  // 迭代次数
  const epochCountsValue = Math.floor(Number(epochCounts.value))
  // 每多少个数据提取一个验证集
  const valPerCountValue = (Number(valPerCount.value) < 0) && (Number(valPerCount.value) > 1) ?
    0 : Number(valPerCount.value)
  // 是否乱序
  const isShuffleValue = Boolean(isShuffle.value)
  // 搞个时间戳，用于计算训练时间
  const startTime = (new Date()).getTime()
  // 记录开始
  logTrainArr(`开始训练模型。`)
  /**
   * @训练模型
   * fit()拟合/训练。给模型指定训练数据和训练参数，返回训练结果
   * trainLog就是每次log的结果堆在一起的tf张量数组
   */
  trainObj.log = await modelObj.model.fit(TensorObj.feature, TensorObj.label, {
    // 批次大小，每次训练迭代的采样数，tf默认32
    batchSize: batchesValue,
    // 迭代次数，即训练轮数
    epochs: epochCountsValue,
    // 用于从训练集中构建验证集的比例
    validationSplit: valPerCountValue,
    // 每次迭代前是否随机打乱数据
    shuffle: isShuffleValue,
    // 回调
    callbacks: [
      // 每轮训练结束的回调
      { onEpochEnd: (epoch, logs) => {
        // 每5次执行一次
        if (epoch % 5 === 4) {
          // 记录
          // console.log(`完成${ epoch + 1 }轮`)
          let log =
            `已完成 ${ epoch + 1 } / ${ epochCountsValue } 轮迭代：
            训练集损失值 = ${ logs.loss }`
          // 如果验证集存在，就补充数据
          if (logs.val_loss !== undefined) {
            log = log + `
              验证集损失值 = ${ logs.val_loss }`
            if (logs.loss > logs.val_loss) {
              log = log + "\n迭代正常"
            } else {
              log = log + "\n！请注意过拟合情况"
            }
          }
          logTrainArr(log)
        }
      }},
    ],
    // 捕获错误信息
  }).catch((err) => {
    console.log("模型训练报错：", err)
    logTrainArr(
      `模型训练出错！
      ${ err }`
    )
    return false
  })
  // 训练结束，计算训练时间
  const endTime = (new Date()).getTime()
  // 如果训练日志存在
  if (trainObj.log) {
    // 训练过程的控制台记录 + 前端记录
    // console.log(`训练结束，记录为：${ JSON.stringify(trainLog) }`)
    logTrainArr(`模型训练完成，耗时 ${ (endTime - startTime) / 1000 } 秒，详情见图表。`)
    // // 可视化模型各层参数
    // showModelLayers(modelObj.model, "训练参数")
    // 可视化模型训练过程
    tfvis.show.history(
      // 容器
      {
        tab: "训练历程",
        name: "评价指标：均方误差 & 平均绝对误差（训练集 & 验证集）",
      },
      // 数据内容
      trainObj.log,
      // 数据分组
      trainObj.log.params.metrics,
    )
    // 打开图表板
    showVisor()
    // ref标记
    isTrained.value = true
  // 如果训练日志不存在，即训练失败
  } else {
    logTrainArr(`模型训练终止（失败），请刷新页面后重试。`)
  }
  // ref标记
  isOnTrain.value = false
}


/**
 * @downloadModel 保存模型
 * @function
 */
function downloadModel() { try {
  // 直接调用现成方法
  modelObj.model.save("downloads://model")
} catch (error) {
  console.error("saveTrainLog()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "保存训练记录报错",
    content: error,
  })
}}


/**
 * @saveTrainLog 保存训练记录
 * @function
 */
 function saveTrainLog() { try {
  // 直接调用现成方法
  downloadTrainHistoryToXlsx(trainObj.log)
} catch (error) {
  console.error("saveTrainLog()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "保存训练记录报错",
    content: error,
  })
}}


/**
 * @logTrainArr 训练过程记录
 * @function
 * @param { String } context 记录内容
 * @note 会读写logArrRef
 */
function logTrainArr(context) {
  if (logArrRef.value.length >= 10) {
    logArrRef.value.shift()
  }
  logArrRef.value.push(context)
}

</script>
